/*
 * Copyright (c) 2016 Mario de Sousa (msousa@fe.up.pt)
 *
 * This file is part of the Modbus library for Beremiz and matiec.
 *
 * This Modbus library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this Modbus library.  If not, see <http://www.gnu.org/licenses/>.
 *
 * This code is made available on the understanding that it will not be
 * used in safety-critical situations without a full and competent review.
 */

/* sin_util.c */
#ifdef MODBUS_TCP

#include <sys/types.h>

#include <errno.h>   // errno
#include <stdlib.h>  // strtoll()
#include <ctype.h>   // isspace()
#include <string.h>  // memcpy(), memset()
#include <strings.h> // strcasecmp()
#include <stdio.h>   // perror()

#ifdef linux_x86_64
#include <netdb.h>
#endif

#ifdef USE_LWIP

#include <lwip/netdb.h>
#include "lwip/sockets.h"

#endif

#include "sin_util.h"

#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff
#endif

#define SOCK_PACKET 4
/* Last time I (msousa) checked, this was not being defined in QNX */
#ifndef socklen_t
#ifndef USE_LWIP
typedef unsigned int socklen_t;
#endif
#endif
typedef unsigned short u16;

static inline int str_is_whitespace(const char *str) {
    for (const char *s = str; *s; s++)
        if (!isspace(*s))
            return 0; // non whitespace char found
    return 1;         // all whitespace, or empty ""
}

/* Convert a string to a number, allowing for leading and trailing whitespace */
/* Number may be in decimal, hexadecimal (leading '0x') or octal (leading '0') format */
static long long str_to_portnum(const char *str) {
    long long port = 0;
    char *errstr;
#define PORT_MIN 0
#define PORT_MAX 65535
    errno = 0; // strtoll() does not set errno to 0 on success!!
    port = strtoll(str, &errstr, 0 /* accept base 8, 10 and 16 */);
    if (str == errstr)
        return -1; // not a number
    if (errno == ERANGE)
        return -2; // out of range
    if (!str_is_whitespace(errstr))
        return -1; // not a number (has trailing characters)
    if ((port < PORT_MIN) || (port > PORT_MAX))
        return -2; // out of range
    return port;
}

static int gettypebyname(const char *protocol) {
    if (strcasecmp(protocol, "tcp") == 0)
        return SOCK_STREAM;
    if (strcasecmp(protocol, "udp") == 0)
        return SOCK_DGRAM;
    if (strcasecmp(protocol, "raw") == 0)
        return SOCK_RAW;
    return SOCK_PACKET; // a deprecated service type, we use here as error.
}

static int getportbyname(in_port_t *port, const char *service, const char *protocol) {
#ifndef USE_LWIP
//    struct servent *se;
#endif
    long long tmp;
    //  if service is NULL, "" or string of whitespace, then set port to 0, and return -1.
    //  Used when binding to a random port on the local host...
    if (port == NULL) {
        return -1;
    }
    if (service == NULL) {
        *port = 0;
        return -1;
    }
    if (str_is_whitespace(service)) {
        *port = 0;
        return -1;
    }
#ifndef USE_LWIP
//    if ((se = getservbyname(service, protocol)) != NULL)
//    {
//        *port = se->s_port;
//        return 0;
//    }
#endif
    if ((tmp = str_to_portnum(service)) >= 0) {
        *port = htons((u16) tmp);
        return 0;
    }
    return -2;
}

static int getipbyname(struct in_addr *ip_addr, const char *host) {
    const struct hostent *he;
    // if host is NULL, "", or "*", then set ip_addr to INADDR_ANY, and return -1.
    // Used when binding to all interfaces on the local host...
    if (host == NULL) {
        ip_addr->s_addr = INADDR_ANY;
        return -1;
    }
    if (str_is_whitespace(host)) {
        ip_addr->s_addr = INADDR_ANY;
        return -1;
    }
    if (strcmp(host, "*") == 0) {
        ip_addr->s_addr = INADDR_ANY;
        return -1;
    }
//    if ((he = gethostbyname(host)) != NULL) {
//        memcpy((char *) ip_addr, he->h_addr, he->h_length);
//        return 0;
//    }
    if ((ip_addr->s_addr = inet_addr(host)) != INADDR_NONE) {
        return 0;
    }
    return -2;
}

int sin_initaddr(struct sockaddr_in *sin,
                 const char *host, int allow_null_host,    // 1 => allow host NULL, "" or "*" -> INADDR_ANY
                 const char *service, int allow_null_serv, // 1 => allow serivce NULL or ""   -> port = 0
                 const char *protocol) {
    int he = allow_null_host ? -1 : 0;
    int se = allow_null_serv ? -1 : 0;

    /* 地址结构大小                    */
    /*
     * @bug error: 'struct sockaddr_in' has no member named 'sin_len'#windows_x86
     */
    memset((void *) sin, 0, sizeof(sin));
    sin->sin_family = AF_INET; /* 地址族                          */
    /* 绑定服务器端口                  */
    if (getportbyname(&(sin->sin_port), service, protocol) < se)
        return -1;
    if (getipbyname(&(sin->sin_addr), host) < he)
        return -1;
    return 0;
}

/* Create a socket for the IP protocol family, and connect to remote host. */
int sin_connsock(const char *host, const char *service, const char *protocol) {
    struct sockaddr_in sin;
    int s;
    int type;

    if (sin_initaddr(&sin, host, 0, service, 0, protocol) < 0)
        return -1;
    if ((type = gettypebyname(protocol)) == SOCK_PACKET)
        return -1;
    /* create the socket */
    if ((s = socket(PF_INET, type, 0)) < 0) {
        perror("socket()");
        return -1;
    }
    /* connect the socket */
    if (connect(s, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
        perror("connect()");
        return -1;
    }

    return s;
}

/* Create a socket for the IP protocol family, and bind to local host's port. */
int sin_bindsock(const char *host, const char *service, const char *protocol) {
    struct sockaddr_in sin;
    int s;
    int type;

    if (sin_initaddr(&sin, host, 1, service, 0, protocol) < 0)
        return -1;
    if ((type = gettypebyname(protocol)) == SOCK_PACKET)
        return -1;
    /* create the socket */
    if ((s = socket(PF_INET, type, 0)) < 0) {
        perror("socket()");
        return -1;
    }

#ifdef windows_x86
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) < 0)
    {
        perror("setsockopt(SO_REUSEADDR) failed");
        return -1;
    }
#endif
    /* bind the socket */
    int res = bind(s, (struct sockaddr *) &sin, sizeof(sin));
    if (res < 0) {
#ifdef windows_x86
        res = WSAGetLastError();
#endif
        printf("bind:%d", res);
        return -1;
    }

    return s;
}

#endif